package org.altervista.cp.thinbcgen.proxy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeSet;

import com.sun.org.apache.bcel.internal.Constants;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.generic.ANEWARRAY;
import com.sun.org.apache.bcel.internal.generic.ArrayType;
import com.sun.org.apache.bcel.internal.generic.BasicType;
import com.sun.org.apache.bcel.internal.generic.BranchInstruction;
import com.sun.org.apache.bcel.internal.generic.ClassGen;
import com.sun.org.apache.bcel.internal.generic.ConstantPoolGen;
import com.sun.org.apache.bcel.internal.generic.FieldGen;
import com.sun.org.apache.bcel.internal.generic.Instruction;
import com.sun.org.apache.bcel.internal.generic.InstructionConstants;
import com.sun.org.apache.bcel.internal.generic.InstructionFactory;
import com.sun.org.apache.bcel.internal.generic.InstructionHandle;
import com.sun.org.apache.bcel.internal.generic.InstructionList;
import com.sun.org.apache.bcel.internal.generic.MULTIANEWARRAY;
import com.sun.org.apache.bcel.internal.generic.MethodGen;
import com.sun.org.apache.bcel.internal.generic.NEWARRAY;
import com.sun.org.apache.bcel.internal.generic.ObjectType;
import com.sun.org.apache.bcel.internal.generic.PUSH;
import com.sun.org.apache.bcel.internal.generic.ReferenceType;
import com.sun.org.apache.bcel.internal.generic.Type;

@SuppressWarnings("unchecked")
public class SunProxyGenerator extends ProxyGenerator implements Constants {

	protected SunProxyGenerator(Class targetClass, Object[] args) {
		super(targetClass, (ExceptionHandler) args[0]);
	}

	protected byte[] generateClass(String generatedClassName, Object[] args) {
		_generatedClassName = generatedClassName;

		_cg = new ClassGen(_generatedClassName, _targetClass.getName(),
				_generatedClassName.substring(_generatedClassName
						.lastIndexOf(".") + 1)
						+ ".java", ACC_PUBLIC | ACC_SUPER,
				new String[] { UpdatableProxy.class.getName() });
		_cp = _cg.getConstantPool();
		_factory = new InstructionFactory(_cg, _cp);

		addBaseMethods();
		addProxyFields();
		addProxyMethods(args[0].getClass());

		return getBytes(_cg);
	}

	private void addBaseMethods() {
		_il = new InstructionList();
		MethodGen method = new MethodGen(ACC_PUBLIC, Type.VOID, new Type[] {
				Type.OBJECT, _ehType }, new String[] { "source", "eh" },
				"<init>", _generatedClassName, _il, _cp);
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
		_il.append(_factory.createInvoke(_targetClass.getName(), "<init>",
				Type.VOID, Type.NO_ARGS, INVOKESPECIAL));
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 1));
		_il.append(_factory.createFieldAccess(_generatedClassName, "_source",
				Type.OBJECT, PUTFIELD));
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 2));
		_il.append(_factory.createFieldAccess(_generatedClassName, "_eh",
				_ehType, PUTFIELD));
		_il.append(InstructionFactory.createReturn(Type.VOID));
		addMethodGen(method);

		_il = new InstructionList();
		method = new MethodGen(ACC_PUBLIC, Type.VOID,
				new Type[] { Type.OBJECT }, new String[] { "source" }, "proxy",
				_generatedClassName, _il, _cp);
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 1));
		_il.append(_factory.createFieldAccess(_generatedClassName, "_source",
				Type.OBJECT, PUTFIELD));
		_il.append(InstructionFactory.createReturn(Type.VOID));
		addMethodGen(method);

		_il = new InstructionList();
		method = new MethodGen(ACC_PUBLIC, Type.OBJECT, Type.NO_ARGS,
				new String[] {}, "unproxy", _generatedClassName, _il, _cp);
		_il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
		_il.append(_factory.createFieldAccess(_generatedClassName, "_source",
				Type.OBJECT, GETFIELD));
		_il.append(InstructionFactory.createReturn(Type.OBJECT));
		addMethodGen(method);
	}

	private void addProxyMethods(Class sourceImplClass) {
		Collection targetMethods = getMethods(_targetClass);
		Collection sourceMethods = getMethods(sourceImplClass);

		for (Iterator targetIt = targetMethods.iterator(); targetIt.hasNext();) {
			MethodWrapper mw = ((MethodWrapper) targetIt.next());
			Method m = mw._m;
			if (Modifier.isAbstract(m.getModifiers())) {
				Type[] argTypes = getTypes(m.getParameterTypes());
				Type retType = getType(m.getReturnType());

				_il = new InstructionList();
				MethodGen method = new MethodGen(ACC_PUBLIC, retType, argTypes,
						getArgumentNames(argTypes.length), m.getName(),
						_generatedClassName, _il, _cp);
				MethodWrapper sourceMethodWrapper = null;
				for (Iterator it = sourceMethods.iterator(); it.hasNext();) {
					MethodWrapper smw = ((MethodWrapper) it.next());
					if (smw.equals(m)) {
						sourceMethodWrapper = smw;
						break;
					}
				}
				if (sourceMethodWrapper == null
						|| Modifier.isAbstract(sourceMethodWrapper._m
								.getModifiers())) {
					_il.append(_factory
							.createNew(UnsupportedOperationException.class
									.getName()));
					_il.append(InstructionConstants.DUP);
					_il.append(new PUSH(_cp, "Method " + m
							+ " is not implemented by "
							+ sourceImplClass.getName()));
					_il.append(_factory.createInvoke(
							UnsupportedOperationException.class.getName(),
							"<init>", Type.VOID, new Type[] { Type.STRING },
							INVOKESPECIAL));
					_il.append(InstructionConstants.ATHROW);
				} else {
					addProxyMethod(method, sourceMethodWrapper._sourceType,
							handleExceptions(m, sourceMethods));
				}

				addMethodGen(method);
			}
		}
	}

	private void addProxyMethod(MethodGen method, ObjectType sourceType,
			boolean handleExceptions) {
		Type[] argTypes = method.getArgumentTypes();
		Type retType = method.getReturnType();

		InstructionList il = method.getInstructionList();

		InstructionHandle ih1 = il.append(InstructionFactory.createLoad(
				Type.OBJECT, 0));
		il.append(_factory.createFieldAccess(_generatedClassName, "_source",
				Type.OBJECT, GETFIELD));
		_il.append(_factory.createCheckCast(sourceType));
		int totalSize = 0;
		for (int j = 0; j < argTypes.length; j++) {
			il
					.append(InstructionFactory.createLoad(argTypes[j],
							totalSize + 1));
			totalSize += argTypes[j].getSize();
		}
		InstructionHandle ih2 = il.append(_factory.createInvoke(sourceType
				.getClassName(), method.getName(), retType, argTypes,
				INVOKEINTERFACE));
		if (!retType.equals(Type.VOID)) {
			il.append(InstructionFactory.createReturn(retType));
		}

		if (handleExceptions) {
			BranchInstruction gotobi = null;
			if (retType.equals(Type.VOID)) {
				gotobi = InstructionFactory.createBranchInstruction(GOTO, null);
				il.append(gotobi);
			}
			InstructionHandle ih3 = il.append(InstructionFactory.createStore(
					Type.OBJECT, totalSize + 1));
			il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
			il.append(_factory.createFieldAccess(_generatedClassName, "_eh",
					_ehType, GETFIELD));
			il.append(InstructionFactory.createLoad(Type.OBJECT, 0));
			il
					.append(InstructionFactory.createLoad(Type.OBJECT,
							totalSize + 1));
			il.append(new PUSH(_cp, method.getName()));
			il.append(new PUSH(_cp, argTypes.length));
			il.append(createNewArray(Type.OBJECT, (short) 1));
			totalSize = 0;
			for (int j = 0; j < argTypes.length; j++) {
				il.append(InstructionConstants.DUP);
				il.append(new PUSH(_cp, j));
				il.append(InstructionFactory.createLoad(argTypes[j],
						totalSize + 1));
				if (argTypes[j] instanceof BasicType) {
					String wcn = getWrapperClassName(argTypes[j]);
					il.append(_factory.createInvoke(wcn, "valueOf",
							new ObjectType(wcn), new Type[] { argTypes[j] },
							INVOKESTATIC));
				}
				il.append(InstructionConstants.AASTORE);
				totalSize += argTypes[j].getSize();
			}
			il.append(_factory.createInvoke(ExceptionHandler.class.getName(),
					"handle", Type.OBJECT, new Type[] {
							new ObjectType(Proxy.class.getName()),
							new ObjectType(Throwable.class.getName()),
							Type.STRING, new ArrayType(Type.OBJECT, 1) },
					INVOKEINTERFACE));
			if (retType.equals(Type.VOID)) {
				il.append(InstructionConstants.POP);
				InstructionHandle ih4 = il.append(InstructionFactory
						.createReturn(Type.VOID));
				gotobi.setTarget(ih4);
			} else {
				if (retType instanceof BasicType) {
					String wcn = getWrapperClassName(retType);
					il.append(_factory.createCheckCast(new ObjectType(wcn)));
					il.append(_factory.createInvoke(wcn, TYPE_NAMES[retType
							.getType()]
							+ "Value", retType, Type.NO_ARGS, INVOKEVIRTUAL));
				} else {
					il
							.append(_factory
									.createCheckCast((ReferenceType) retType));
				}
				il.append(InstructionFactory.createReturn(retType));
			}
			method.addExceptionHandler(ih1, ih2, ih3, new ObjectType(
					Throwable.class.getName()));
		} else if (retType.equals(Type.VOID)) {
			il.append(InstructionFactory.createReturn(Type.VOID));
		}

	}

	private void addProxyFields() {
		FieldGen field = new FieldGen(ACC_PRIVATE, Type.OBJECT, "_source", _cp);
		_cg.addField(field.getField());

		field = new FieldGen(ACC_PRIVATE, _ehType, "_eh", _cp);
		_cg.addField(field.getField());
	}

	private void addMethodGen(MethodGen method) {
		method.setMaxStack();
		method.setMaxLocals();
		_cg.addMethod(method.getMethod());
		_il.dispose();
	}

	private boolean handleExceptions(Method m, Collection sourceMethods) {
		for (Iterator it = sourceMethods.iterator(); it.hasNext();) {
			MethodWrapper smw = (MethodWrapper) it.next();
			if (smw.equals(m)) {
				return !Arrays.equals(smw._m.getExceptionTypes(), m
						.getExceptionTypes());
			}
		}
		return false;
	}

	private MethodSet getMethods(Class c) {
		MethodSet ms = getMethods(c, new MethodSet());
		return ms;
	}

	private MethodSet getMethods(Class c, MethodSet scannedMethodSet) {
		MethodSet methodSet = new MethodSet();
		Method[] methods = c.getMethods();
		for (int i = 0; i < methods.length; i++) {
			if (!Modifier.isStatic(methods[i].getModifiers())) {
				MethodWrapper mw = scannedMethodSet.extract(methods[i]);
				if (mw == null
						|| (Modifier.isAbstract(mw._m.getModifiers()) && !Modifier
								.isAbstract(methods[i].getModifiers()))) {
					if (mw != null) {
						methodSet.remove(mw);
					}
					if (!methods[i].getName().equals("proxy")
							&& !methods[i].getName().equals("unproxy")) {
						methodSet.add(methods[i]);
					}
				}

				scannedMethodSet.add(methods[i]);
			}
		}

		return methodSet;
	}

	private static final String getWrapperClassName(Type type) {
		switch (type.getType()) {
		case Constants.T_VOID:
			return Void.class.getName();
		case Constants.T_BOOLEAN:
			return Boolean.class.getName();
		case Constants.T_BYTE:
			return Byte.class.getName();
		case Constants.T_SHORT:
			return Short.class.getName();
		case Constants.T_CHAR:
			return Character.class.getName();
		case Constants.T_INT:
			return Integer.class.getName();
		case Constants.T_LONG:
			return Long.class.getName();
		case Constants.T_DOUBLE:
			return Double.class.getName();
		case Constants.T_FLOAT:
			return Float.class.getName();
		default:
			return null;
		}
	}

	private static final byte[] getBytes(ClassGen cg) {
		Repository.addClass(cg.getJavaClass());

		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		try {
			cg.getJavaClass().dump(baos);
			/*
			 * java.io.FileOutputStream fos = new
			 * java.io.FileOutputStream("/tmp/" + cg.getClassName() + ".class");
			 * cg.getJavaClass().dump(fos); fos.close();
			 */
		} catch (IOException e) {
		}
		return baos.toByteArray();
	}

	private static final boolean methodEquals(Method m1, Method m2) {
		return m1.getName().equals(m2.getName())
				&& Arrays
						.equals(m1.getParameterTypes(), m2.getParameterTypes());
	}

	private static final String[] getArgumentNames(int length) {
		String[] argNames = new String[length];
		for (int i = 0; i < argNames.length; i++) {
			argNames[i] = "arg" + i;
		}
		return argNames;
	}

	private static final Type[] getTypes(Class[] types) {
		Type[] t = new Type[types.length];
		for (int i = 0; i < t.length; i++) {
			t[i] = getType(types[i]);
		}
		return t;
	}

	private Instruction createNewArray(Type t, short dim) {
		if (dim == 1) {
			if (t instanceof ObjectType)
				return new ANEWARRAY(_cp.addClass((ObjectType) t));
			else if (t instanceof ArrayType)
				return new ANEWARRAY(_cp.addArrayClass((ArrayType) t));
			else
				return new NEWARRAY(((BasicType) t).getType());
		} else {
			ArrayType at;

			if (t instanceof ArrayType)
				at = (ArrayType) t;
			else
				at = new ArrayType(t, dim);

			return new MULTIANEWARRAY(_cp.addArrayClass(at), dim);
		}
	}

	private static final Type getType(Class type) {
		if (type.isArray()) {
			return new ArrayType(getType(type.getComponentType()), 1);
		} else if (type == null || void.class.equals(type)) {
			return Type.VOID;
		} else if (boolean.class.equals(type)) {
			return Type.BOOLEAN;
		} else if (byte.class.equals(type)) {
			return Type.BYTE;
		} else if (short.class.equals(type)) {
			return Type.SHORT;
		} else if (int.class.equals(type)) {
			return Type.INT;
		} else if (long.class.equals(type)) {
			return Type.LONG;
		} else if (float.class.equals(type)) {
			return Type.FLOAT;
		} else if (double.class.equals(type)) {
			return Type.DOUBLE;
		} else if (char.class.equals(type)) {
			return Type.CHAR;
		} else {
			return new ObjectType(type.getName());
		}
	}

	private InstructionFactory _factory;
	private ConstantPoolGen _cp;
	private ClassGen _cg;
	private InstructionList _il;

	// private ObjectType _sourceType;
	private ObjectType _ehType = new ObjectType(ExceptionHandler.class
			.getName());

	private String _generatedClassName;

	private class MethodWrapper implements Comparable {
		public int compareTo(Object o) {
			if (o instanceof Method || o instanceof MethodWrapper) {
				if (equals(o)) {
					return 0;
				} else {
					return 1;
				}
			}
			return 1;
		}

		public boolean equals(Object o) {
			if (o instanceof MethodWrapper) {
				return methodEquals(_m, ((MethodWrapper) o)._m);
			} else {
				return methodEquals(_m, (Method) o);
			}
		}

		public int hashCode() {
			return _m.hashCode();
		}

		public String toString() {
			return _m.toString();
		}

		MethodWrapper(Object o) {
			_m = (Method) o;
			Class sourceClass = findSourceClass(_m);
			if (sourceClass != null) {
				_sourceType = new ObjectType(sourceClass.getName());
			}
		}

		private Class findSourceClass(Method m) {
			Class c = m.getDeclaringClass();
			while (c != null) {
				Class[] interfaces = c.getInterfaces();
				for (int i = 0; i < interfaces.length; i++) {
					Method[] imethods = interfaces[i].getMethods();
					for (int j = 0; j < imethods.length; j++) {
						if (methodEquals(m, imethods[j])) {
							return interfaces[i];
						}
					}
				}
				c = c.getSuperclass();
			}
			return null;
		}

		Method _m;
		ObjectType _sourceType;
	}

	@SuppressWarnings("serial")
	private class MethodSet extends TreeSet {

		public boolean add(Object o) {
			return super.add(getMethodWrapper(o));
		}

		public MethodWrapper extract(Object o) {
			MethodWrapper mw = getMethodWrapper(o);
			for (Iterator it = iterator(); it.hasNext();) {
				MethodWrapper imw = (MethodWrapper) it.next();
				if (imw.equals(mw))
					return imw;
			}
			return null;
		}

		public boolean addAll(Collection c) {
			boolean result = false;
			for (Iterator it = c.iterator(); it.hasNext();) {
				result = add(getMethodWrapper(it.next())._m) || result;
			}
			return result;
		}

		public boolean remove(Object o) {
			return super.remove(getMethodWrapper(o));
		}

		private MethodWrapper getMethodWrapper(Object o) {
			return o instanceof MethodWrapper ? (MethodWrapper) o
					: new MethodWrapper(o);
		}
	}

}
